前面都是在做資料處理,所以只有程式碼,沒有 UI 畫面,謝謝看到今天的朋友
台股光是上市的家數是超過1000 家,是不可能在一個手機上顯示所有公司的基本資料的,在手機上,我們常使用 UITableView 來呈現大數量,且格式相近的資料。
依照 Apple MVC 的框架,每個人的角色分配如下
Model: 負責處理邏輯,不會直接和 View 進行溝通
View: 負責呈現資料,不會直接和 Model 進行溝通
Controller: 成為 Model 和 View 的中間人,當有 View 需要資料的時候,負責提供資料。如果 View 被點擊,則處理後續的點擊事件。當 Model 收到資料時,Controller 成為 Model 通知的對象。**
這個頁面現在要呈現 上市/上櫃/興櫃 的公司基本資料,而這些基本資料需要從雲端下載。那這個頁面的 MVC 職責大概是這樣。
**Model: 負責下載資料,並儲存下載後的資料。在實際專案的時候,通常還會針對這種不會馬上變化的資料,進行快取。但這邊因為不是 key feature,所以不進行快取的實作。但如果要做的話,把資料放在 UserDefaults 或是 CoreData 裡面就可以做到了。
View: 在 VC 的 RootView 下,主要呈現公司基本資料的列表。但設計上,我不想要在 VC 的生命週期中直接發動 URLRequest,這一段,我希望用 button 的 action 來發動。第一階段希望做成用 button 來發動下載,這樣比較容易說明每個動作,也比較容易解說每一個 response 後做的行為。而 TableViewCell 裡,想呈現的資訊是,股票名,股票代號,資本額。
Controller: 在生命週期中,並不特別做什麼。但是當 button 按下的時候,會去呼叫 model 進行對應的資料下載。當 model 資料有變更的時候,會呼叫 tableView.reloadData(),去更新列表。
基本的 data model 設計,這邊會需要 conform Hashable,是因為我習慣用 Set 在更新的時候進行資料的合併。
import Foundation
struct StockBasicInfo: Hashable {
let stockCode: String
let stockName: String
let companyName: String
let capital: String
}
基本的 UI layout 設計如下。
創建一個 RequestBasicInfoViewController
import UIKit
class RequestBasicInfoViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
}
先在一開始的 LandingViewController 拉一個 button,會發動 navigationController push 轉場
@IBAction func pushRequestBasicInfoVC(_ sender: Any) {
let storyboard = UIStoryboard(name: "RequestBasicInfo", bundle: nil)
if let vc = storyboard.instantiateViewController(withIdentifier: "RequestBasicInfoViewController") as? RequestBasicInfoViewController {
navigationController?.pushViewController(vc, animated: true)
}
}
接下來,進行 Model 的實作,這邊我選擇使用 delegate pattern 來通知 VC 資料已經下載好了。就前面所述,Model 要處理邏輯,需要處理的部分如下。
enum MarketType: String {
case twStock = "上市"
case otc = "上櫃"
case emerging = "興櫃"
}
import Foundation
protocol RequestBasicInfoModelDelegate: AnyObject {
func didRecieveCompanyInfo(_ companyList: [StockBasicInfo], error: Error?)
}
class RequestBasicInfoModel {
weak var delegate: RequestBasicInfoModelDelegate?
private var recievedInfo = [MarketType]()
private var companyList = [StockBasicInfo]()
var count: Int {
return companyList.count
}
private lazy var stockInfoManager: StockInfoManager = {
let manager = StockInfoManager()
return manager
}()
func getStockInfo(at indexPath: IndexPath) -> StockBasicInfo? {
let index = indexPath.row
if companyList.indices.contains(index) {
return companyList[index]
}
return nil
}
func requestTwStock() {
if recievedInfo.contains(.twStock) {
print("已經拿過資料")
return
}
stockInfoManager.requestTwStockCodeAndName { [weak self] list, error in
self?.updateStockInfo(from: list, marketType: .twStock)
self?.delegate?.didRecieveCompanyInfo(list, error: error)
}
}
func requestOTCStock() {
if recievedInfo.contains(.otc) {
print("已經拿過資料")
return
}
stockInfoManager.requestOTCCodeAndName { [weak self] list, error in
self?.updateStockInfo(from: list, marketType: .otc)
self?.delegate?.didRecieveCompanyInfo(list, error: error)
}
}
func requestEmergingStock() {
if recievedInfo.contains(.emerging) {
print("已經拿過資料")
return
}
stockInfoManager.requestEmerginCodeAndName { [weak self] list, error in
self?.updateStockInfo(from: list, marketType: .emerging)
self?.delegate?.didRecieveCompanyInfo(list, error: error)
}
}
private func updateStockInfo(from list: [StockBasicInfo], marketType: MarketType) {
recievedInfo.append(marketType)
let recievedList = Set(list)
let updatedList = Set(companyList).union(recievedList)
companyList = Array(updatedList).sorted { $0.stockCode < $1.stockCode }
}
}
UITableView 的程式碼,這邊 custom 一個 TableViewCell,CompanyBasicInfoTableViewCell
class CompanyBasicInfoTableViewCell: UITableViewCell {
static let identifier = "CompanyBasicInfoTableViewCell"
@IBOutlet weak var codeAndNameLabel: UILabel!
@IBOutlet weak var capitalLabel: UILabel!
}
UITableView 如果設置上有遇到困難,這邊有 Apple 文件
Apple 對於 UITableView 的說明文件
ViewController 部分的程式碼
import UIKit
class RequestBasicInfoViewController: UIViewController {
@IBOutlet weak var stateLabel: UILabel!
@IBOutlet weak var tableView: UITableView!
private lazy var model: RequestBasicInfoModel = {
let model = RequestBasicInfoModel()
model.delegate = self
return model
}()
// MARK: - life cycle
override func viewDidLoad() {
super.viewDidLoad()
setupUI()
}
// MARK: - private methods
private func setupUI() {
tableView.delegate = self
tableView.dataSource = self
}
// MARK: - IBAction
@IBAction func requestTwStockButtonDidTap(_ sender: Any) {
model.requestTwStock()
}
@IBAction func requestOTCButtonDidTap(_ sender: Any) {
model.requestOTCStock()
}
@IBAction func requestEmergingButtonDidTap(_ sender: Any) {
model.requestEmergingStock()
}
}
extension RequestBasicInfoViewController: UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return model.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: CompanyBasicInfoTableViewCell.identifier, for: indexPath) as? CompanyBasicInfoTableViewCell,
let info = model.getStockInfo(at: indexPath) else {
return UITableViewCell()
}
let codeName = "\(info.stockName) - (\(info.stockCode))\n\(info.companyName)"
let capital = "資本額: \(info.capital) 元"
cell.codeAndNameLabel.text = codeName
cell.capitalLabel.text = capital
return cell
}
}
extension RequestBasicInfoViewController: RequestBasicInfoModelDelegate {
func didRecieveCompanyInfo(_ companyList: [StockBasicInfo], error: Error?) {
if let error = error {
print("basic info reqeust got error: \(error.localizedDescription)")
return
}
updateStateUI()
tableView.reloadData()
}
private func updateStateUI() {
var recievedMarketsText = ""
for market in model.recievedInfo {
recievedMarketsText += "\(market.rawValue) "
}
stateLabel.text = "已取得 \(recievedMarketsText) 資料 - 數量 \(model.count) 筆"
}
}
完成後的狀態
延伸功能:
UITableView 如果設置上有遇到困難,這邊有 Apple 文件
Apple 對於 UITableView 的說明文件
通常在列表的 UI 上,點擊後,會再推入一個 VC 去呈現這個格子內容的詳細資料。但這邊的 demo 只會做到這裡,詳細頁的 UI 在實作並不是難度很大的功能,比較難的是資料來源。
如果要實作,就在 UITableViewDelegate 的 tableView(_:didSelectRowAt:) 實作拿出 info model,然後把 info model 傳入下一個 vc 即可。
D5 程式碼可以在 GitHub 上下載的到
GitHub Repo